<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="settings.html">
<link rel="import" href="dom-innerHTML.html">
<script>
(function() {
  'use strict';

  var Settings = Polymer.Settings;
  var DomApi = Polymer.DomApi;
  var dom = DomApi.factory;
  var TreeApi = Polymer.TreeApi;
  var getInnerHTML = Polymer.domInnerHTML.getInnerHTML;
  var CONTENT = DomApi.CONTENT;

  // *************** Configure DomApi for Shady DOM!! ***************
  if (Settings.useShadow) {
    return;
  }

  var nativeCloneNode = Element.prototype.cloneNode;
  var nativeImportNode = Document.prototype.importNode;

  Polymer.Base.mixin(DomApi.prototype, {

    _lazyDistribute: function(host) {
      // note: only try to distribute if the root is not clean; this ensures
      // we don't distribute before initial distribution
      if (host.shadyRoot && host.shadyRoot._distributionClean) {
        host.shadyRoot._distributionClean = false;
        Polymer.dom.addDebouncer(host.debounce('_distribute',
          host._distributeContent));
      }
    },

    appendChild: function(node) {
      return this.insertBefore(node);
    },

    // cases in which we may not be able to just do standard native call
    // 1. container has a shadyRoot (needsDistribution if and only if the
    // shadyRoot has an insertion point)
    // 2. container is a shadyRoot (don't distribute, instead set
    // container to container.host.
    // 3. node is <content> (host of container needs distribution)
    insertBefore: function(node, ref_node) {
      if (ref_node && TreeApi.Logical.getParentNode(ref_node) !== this.node) {
        throw Error('The ref_node to be inserted before is not a child ' +
          'of this node');
      }
      // remove node from its current position if and only if it's in a tree.
      if (node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
        var parent = TreeApi.Logical.getParentNode(node);
        // notify existing parent that this node is being removed.
        if (parent) {
          if (DomApi.hasApi(parent)) {
            dom(parent).notifyObserver();
          }
          this._removeNode(node);
        } else {
          this._removeOwnerShadyRoot(node);
        }
      }
      if (!this._addNode(node, ref_node)) {
        if (ref_node) {
          // if ref_node is <content> replace with first distributed node
          ref_node = ref_node.localName === CONTENT ?
            this._firstComposedNode(ref_node) : ref_node;
        }
        // if adding to a shadyRoot, add to host instead
        var container = this.node._isShadyRoot ? this.node.host : this.node;
        if (ref_node) {
          TreeApi.Composed.insertBefore(container, node, ref_node);
        } else {
          TreeApi.Composed.appendChild(container, node);
        }
      }
      this.notifyObserver();
      return node;
    },

    // Try to add node. Record logical info, track insertion points, perform
    // distribution if and only if needed. Return true if the add is handled.
    _addNode: function(node, ref_node) {
      var root = this.getOwnerRoot();
      if (root) {
        // note: we always need to see if an insertion point is added
        // since this saves logical tree info; however, invalidation state
        // needs
        var ipAdded = this._maybeAddInsertionPoint(node, this.node);
        // invalidate insertion points if and only if not already invalid!
        if (!root._invalidInsertionPoints) {
          root._invalidInsertionPoints = ipAdded;
        }
        this._addNodeToHost(root.host, node);
      }
      if (TreeApi.Logical.hasChildNodes(this.node)) {
        TreeApi.Logical.recordInsertBefore(node, this.node, ref_node);
      }
      // if not distributing and not adding to host, do a fast path addition
      var handled = this._maybeDistribute(node) ||
        this.node.shadyRoot;
      // if shady is handling this node,
      // the actual dom may not be removed if the node or fragment contents
      // remain undistributed so we ensure removal here.
      // NOTE: we only remove from existing location if and only if shady dom is
      // involved.
      // This is because a node fragment is passed to the native add method
      // which expects to see fragment children. Regular elements must also
      // use this check because not doing so causes separation of
      // attached/detached and breaks, for example,
      // dom-if's attached/detached checks.
      if (handled) {
        if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
          while (node.firstChild) {
            TreeApi.Composed.removeChild(node, node.firstChild);
          }
        } else {
          var parent = TreeApi.Composed.getParentNode(node);
          if (parent) {
            TreeApi.Composed.removeChild(parent, node);
          }
        }
      }
      return handled;
    },

    /**
      Removes the given `node` from the element's `lightChildren`.
      This method also performs dom composition.
    */
    removeChild: function(node) {
      if (TreeApi.Logical.getParentNode(node) !== this.node) {
        throw Error('The node to be removed is not a child of this node: ' +
          node);
      }
      if (!this._removeNode(node)) {
        // if removing from a shadyRoot, remove form host instead
        var container = this.node._isShadyRoot ? this.node.host : this.node;
        // not guaranteed to physically be in container; e.g.
        // undistributed nodes.
        var parent = TreeApi.Composed.getParentNode(node);
        if (container === parent) {
          TreeApi.Composed.removeChild(container, node);
        }
      }
      this.notifyObserver();
      return node;
    },

    // Try to remove node: update logical info and perform distribution if and
    // only if needed. Return true if the removal has been handled.
    // note that it's possible for both the node's host and its parent
    // to require distribution... both cases are handled here.
    _removeNode: function(node) {
      // important that we want to do this only if the node has a logical parent
      var logicalParent = TreeApi.Logical.hasParentNode(node) &&
        TreeApi.Logical.getParentNode(node);
      var distributed;
      var root = this._ownerShadyRootForNode(node);
      if (logicalParent) {
        // distribute node's parent if and only if needed
        distributed = dom(node)._maybeDistributeParent();
        TreeApi.Logical.recordRemoveChild(node, logicalParent);
        // remove node from root and distribute it if and only if needed
        if (root && this._removeDistributedChildren(root, node)) {
          root._invalidInsertionPoints = true;
          this._lazyDistribute(root.host);
        }
      }
      this._removeOwnerShadyRoot(node);
      if (root) {
        this._removeNodeFromHost(root.host, node);
      }
      return distributed;
    },

    replaceChild: function(node, ref_node) {
      this.insertBefore(node, ref_node);
      this.removeChild(ref_node);
      return node;
    },

    _hasCachedOwnerRoot: function(node) {
      return Boolean(node._ownerShadyRoot !== undefined);
    },

    getOwnerRoot: function() {
      return this._ownerShadyRootForNode(this.node);
    },

    _ownerShadyRootForNode: function(node) {
      if (!node) {
        return;
      }
      var root = node._ownerShadyRoot;
      if (root === undefined) {
        if (node._isShadyRoot) {
          root = node;
        } else {
          var parent = TreeApi.Logical.getParentNode(node);
          if (parent) {
            root = parent._isShadyRoot ? parent :
              this._ownerShadyRootForNode(parent);
          } else {
           root = null;
          }
        }
        // memo-ize result for performance but only memo-ize a false-y
        // result if node is in the document. This avoids a problem where a root
        // can be cached while an element is inside a fragment.
        // If this happens and we cache the result, the value can become stale
        // because for perf we avoid processing the subtree of added fragments.
        if (root || document.documentElement.contains(node)) {
          node._ownerShadyRoot = root;
        }
      }
      return root;
    },

    _maybeDistribute: function(node) {
      // TODO(sorvell): technically we should check non-fragment nodes for
      // <content> children but since this case is assumed to be exceedingly
      // rare, we avoid the cost and will address with some specific api
      // when the need arises.  For now, the user must call
      // distributeContent(true), which updates insertion points manually
      // and forces distribution.
      var fragContent = (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) &&
        !node.__noContent && dom(node).querySelector(CONTENT);
      var wrappedContent = fragContent &&
        (TreeApi.Logical.getParentNode(fragContent).nodeType !==
        Node.DOCUMENT_FRAGMENT_NODE);
      var hasContent = fragContent || (node.localName === CONTENT);
      // There are 2 possible cases where a distribution may need to occur:
      // 1. <content> being inserted (the host of the shady root where
      //    content is inserted needs distribution)
      // 2. children being inserted into parent with a shady root (parent
      //    needs distribution)
      if (hasContent) {
        var root = this.getOwnerRoot();
        if (root) {
          // note, insertion point list update is handled after node
          // mutations are complete
          this._lazyDistribute(root.host);
        }
      }
      var needsDist = this._nodeNeedsDistribution(this.node);
      if (needsDist) {
        this._lazyDistribute(this.node);
      }
      // Return true when distribution will fully handle the composition
      // Note that if a content was being inserted that was wrapped by a node,
      // and the parent does not need distribution, return false to allow
      // the nodes to be added directly, after which children may be
      // distributed and composed into the wrapping node(s)
      return needsDist || (hasContent && !wrappedContent);
    },

    /* note: parent argument is required since node may have an out
    of date parent at this point; returns true if a <content> is being added */
    _maybeAddInsertionPoint: function(node, parent) {
      var added;
      if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE &&
        !node.__noContent) {
        var c$ = dom(node).querySelectorAll(CONTENT);
        for (var i=0, n, np, na; (i<c$.length) && (n=c$[i]); i++) {
          np = TreeApi.Logical.getParentNode(n);
          // don't allow node's parent to be fragment itself
          if (np === node) {
            np = parent;
          }
          na = this._maybeAddInsertionPoint(n, np);
          added = added || na;
        }
      } else if (node.localName === CONTENT) {
        TreeApi.Logical.saveChildNodes(parent);
        TreeApi.Logical.saveChildNodes(node);
        added = true;
      }
      return added;
    },

    _updateInsertionPoints: function(host) {
      var i$ = host.shadyRoot._insertionPoints =
        dom(host.shadyRoot).querySelectorAll(CONTENT);
      // ensure <content>'s and their parents have logical dom info.
      for (var i=0, c; i < i$.length; i++) {
        c = i$[i];
        TreeApi.Logical.saveChildNodes(c);
        TreeApi.Logical.saveChildNodes(TreeApi.Logical.getParentNode(c));
      }
    },

    _nodeNeedsDistribution: function(node) {
      return node && node.shadyRoot &&
        DomApi.hasInsertionPoint(node.shadyRoot);
    },

    // a node being added is always in this same host as this.node.
    _addNodeToHost: function(host, node) {
      if (host._elementAdd) {
        host._elementAdd(node);
      }
    },

    _removeNodeFromHost: function(host, node) {
      if (host._elementRemove) {
        host._elementRemove(node);
      }
    },

    _removeDistributedChildren: function(root, container) {
      var hostNeedsDist;
      var ip$ = root._insertionPoints;
      for (var i=0; i<ip$.length; i++) {
        var content = ip$[i];
        if (this._contains(container, content)) {
          var dc$ = dom(content).getDistributedNodes();
          for (var j=0; j<dc$.length; j++) {
            hostNeedsDist = true;
            var node = dc$[j];
            var parent = TreeApi.Composed.getParentNode(node);
            if (parent) {
              TreeApi.Composed.removeChild(parent, node);
            }
          }
        }
      }
      return hostNeedsDist;
    },

    _contains: function(container, node) {
      while (node) {
        if (node == container) {
          return true;
        }
        node = TreeApi.Logical.getParentNode(node);
      }
    },

    _removeOwnerShadyRoot: function(node) {
      // optimization: only reset the tree if node is actually in a root
      if (this._hasCachedOwnerRoot(node)) {
        var c$ = TreeApi.Logical.getChildNodes(node);
        for (var i=0, l=c$.length, n; (i<l) && (n=c$[i]); i++) {
          this._removeOwnerShadyRoot(n);
        }
      }
      node._ownerShadyRoot = undefined;
    },

    // TODO(sorvell): This will fail if distribution that affects this
    // question is pending; this is expected to be exceedingly rare, but if
    // the issue comes up, we can force a flush in this case.
    _firstComposedNode: function(content) {
      var n$ = dom(content).getDistributedNodes();
      for (var i=0, l=n$.length, n, p$; (i<l) && (n=n$[i]); i++) {
        p$ = dom(n).getDestinationInsertionPoints();
        // means that we're composed to this spot.
        if (p$[p$.length-1] === content) {
          return n;
        }
      }
    },

    // TODO(sorvell): consider doing native QSA and filtering results.
    querySelector: function(selector) {
      // match selector and halt on first result.
      var result = this._query(function(n) {
        return DomApi.matchesSelector.call(n, selector);
      }, this.node, function(n) {
        return Boolean(n);
      })[0];
      return result || null;
    },

    querySelectorAll: function(selector) {
      return this._query(function(n) {
        return DomApi.matchesSelector.call(n, selector);
      }, this.node);
    },

    getDestinationInsertionPoints: function() {
      return this.node._destinationInsertionPoints || [];
    },

    getDistributedNodes: function() {
      return this.node._distributedNodes || [];
    },

    _clear: function() {
      while (this.childNodes.length) {
        this.removeChild(this.childNodes[0]);
      }
    },

    setAttribute: function(name, value) {
      this.node.setAttribute(name, value);
      this._maybeDistributeParent();
    },

    removeAttribute: function(name) {
      this.node.removeAttribute(name);
      this._maybeDistributeParent();
    },

    _maybeDistributeParent: function() {
      if (this._nodeNeedsDistribution(this.parentNode)) {
        this._lazyDistribute(this.parentNode);
        return true;
      }
    },

    cloneNode: function(deep) {
      var n = nativeCloneNode.call(this.node, false);
      if (deep) {
        var c$ = this.childNodes;
        var d = dom(n);
        for (var i=0, nc; i < c$.length; i++) {
          nc = dom(c$[i]).cloneNode(true);
          d.appendChild(nc);
        }
      }
      return n;
    },

    importNode: function(externalNode, deep) {
      // for convenience use this node's ownerDoc if the node isn't a document
      var doc = this.node instanceof Document ? this.node :
        this.node.ownerDocument;
      var n = nativeImportNode.call(doc, externalNode, false);
      if (deep) {
        var c$ = TreeApi.Logical.getChildNodes(externalNode);
        var d = dom(n);
        for (var i=0, nc; i < c$.length; i++) {
          nc = dom(doc).importNode(c$[i], true);
          d.appendChild(nc);
        }
      }
      return n;
    },

    _getComposedInnerHTML: function() {
      return getInnerHTML(this.node, true);
    }

  });

  Object.defineProperties(DomApi.prototype, {

    activeElement: {
      get: function() {
        var active = document.activeElement;
        if (!active) {
          return null;
        }
        var isShadyRoot = !!this.node._isShadyRoot;
        if (this.node !== document) {
          // If this node isn't a document or shady root, then it doesn't have
          // an active element.
          if (!isShadyRoot) {
            return null;
          }
          // If this shady root's host is the active element or the active
          // element is not a descendant of the host (in the composed tree),
          // then it doesn't have an active element.
          if (this.node.host === active ||
              !this.node.host.contains(active)) {
            return null;
          }
        }
        // This node is either the document or a shady root of which the active
        // element is a (composed) descendant of its host; iterate upwards to
        // find the active element's most shallow host within it.
        var activeRoot = dom(active).getOwnerRoot();
        while (activeRoot && activeRoot !== this.node) {
          active = activeRoot.host;
          activeRoot = dom(active).getOwnerRoot();
        }
        if (this.node === document) {
          // This node is the document, so activeRoot should be null.
          return activeRoot ? null : active;
        } else {
          // This node is a non-document shady root, and it should be
          // activeRoot.
          return activeRoot === this.node ? active : null;
        }
      },
      configurable: true
    },

    childNodes: {
      get: function() {
        var c$ = TreeApi.Logical.getChildNodes(this.node);
        return Array.isArray(c$) ? c$ : TreeApi.arrayCopyChildNodes(this.node);
      },
      configurable: true
    },

    children: {
      get: function() {
        if (TreeApi.Logical.hasChildNodes(this.node)) {
          return Array.prototype.filter.call(this.childNodes, function(n) {
            return (n.nodeType === Node.ELEMENT_NODE);
          });
        } else {
          return TreeApi.arrayCopyChildren(this.node);
        }
      },
      configurable: true
    },

    parentNode: {
      get: function() {
        return TreeApi.Logical.getParentNode(this.node);
      },
      configurable: true
    },

    firstChild: {
      get: function() {
        return TreeApi.Logical.getFirstChild(this.node);
      },
      configurable: true
    },

    lastChild: {
      get: function() {
        return TreeApi.Logical.getLastChild(this.node);
      },
      configurable: true
    },

    nextSibling: {
      get: function() {
        return TreeApi.Logical.getNextSibling(this.node);
      },
      configurable: true
    },

    previousSibling: {
      get: function() {
        return TreeApi.Logical.getPreviousSibling(this.node);
      },
      configurable: true
    },

    firstElementChild: {
      get: function() {
        return TreeApi.Logical.getFirstElementChild(this.node);
      },
      configurable: true
    },

    lastElementChild: {
      get: function() {
        return TreeApi.Logical.getLastElementChild(this.node);
      },
      configurable: true
    },

    nextElementSibling: {
      get: function() {
        return TreeApi.Logical.getNextElementSibling(this.node);
      },
      configurable: true
    },

    previousElementSibling: {
      get: function() {
        return TreeApi.Logical.getPreviousElementSibling(this.node);
      },
      configurable: true
    },

    // textContent / innerHTML
    textContent: {
      get: function() {
        var nt = this.node.nodeType;
        if (nt === Node.TEXT_NODE || nt === Node.COMMENT_NODE) {
          return this.node.textContent;
        } else {
          var tc = [];
          for (var i = 0, cn = this.childNodes, c; (c = cn[i]); i++) {
            if (c.nodeType !== Node.COMMENT_NODE) {
              tc.push(c.textContent);
            }
          }
          return tc.join('');
        }
      },
      set: function(text) {
        var nt = this.node.nodeType;
        if (nt === Node.TEXT_NODE || nt === Node.COMMENT_NODE) {
          this.node.textContent = text;
        } else {
          this._clear();
          if (text) {
            this.appendChild(document.createTextNode(text));
          }
        }
      },
      configurable: true
    },

    innerHTML: {
      get: function() {
        var nt = this.node.nodeType;
        if (nt === Node.TEXT_NODE || nt === Node.COMMENT_NODE) {
          return null;
        } else {
          return getInnerHTML(this.node);
        }
      },
      set: function(text) {
        var nt = this.node.nodeType;
        if (nt !== Node.TEXT_NODE || nt !== Node.COMMENT_NODE) {
          this._clear();
          var d = document.createElement('div');
          d.innerHTML = text;
          // here, appendChild may move nodes async so we cannot rely
          // on node position when copying
          var c$ = TreeApi.arrayCopyChildNodes(d);
          for (var i=0; i < c$.length; i++) {
            this.appendChild(c$[i]);
          }
        }
      },
      configurable: true
    }

  });

  DomApi.hasInsertionPoint = function(root) {
    return Boolean(root && root._insertionPoints.length);
  };

})();
</script>
